// Copyright 2024 syzkaller project authors. All rights reserved.
// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.

package image_test

import (
	"fmt"
	"io"
	"os"
	"path/filepath"
	"strings"
	"testing"

	. "github.com/google/syzkaller/pkg/image"
	"github.com/google/syzkaller/prog"
	"github.com/google/syzkaller/sys/targets"
)

// To get maximum test coverage here, install the following Debian packages:
// dosfstools e2fsprogs btrfs-progs util-linux f2fs-tools jfsutils util-linux
// dosfstools ocfs2-tools reiserfsprogs xfsprogs erofs-utils exfatprogs
// gfs2-utils.

const corruptedFs = "IAmACorruptedFs"

func TestFsck(t *testing.T) {
	target, err := prog.GetTarget(targets.Linux, targets.AMD64)
	if err != nil {
		t.Fatal(err)
	}
	fsckChecker := FsckChecker{}

	// Use the images generated by syz-imagegen as a collection of clean file systems.
	cleanFsProgs, err := filepath.Glob(filepath.Join("..", "sys", "linux", "test", "syz_mount_image_*_0"))
	if err != nil {
		t.Fatalf("directory read failed: %v", err)
	}

	for _, file := range cleanFsProgs {
		sourceProg, err := os.ReadFile(file)
		if err != nil {
			t.Fatal(err)
		}
		p, err := target.Deserialize(sourceProg, prog.NonStrict)
		if err != nil {
			t.Fatalf("failed to deserialize %s: %s", file, err)
		}
		p.ForEachAsset(func(name string, typ prog.AssetType, r io.Reader, c *prog.Call) {
			if c.Meta.Attrs.Fsck == "" {
				return
			}
			fsckCmd := c.Meta.Attrs.Fsck
			// Tolerate missing fsck commands except during CI runs.
			skip := !fsckChecker.Exists(fsckCmd) && os.Getenv("CI") == ""

			fsName := strings.TrimPrefix(c.Meta.Name, "syz_mount_image$")
			// Check that the file system in the image is detected as clean.
			t.Run(fmt.Sprintf("clean %s", fsName), func(t *testing.T) {
				if skip {
					t.Skipf("%s not available", fsckCmd)
				}

				logs, isClean, err := Fsck(r, fsckCmd)
				if err != nil {
					t.Fatalf("failed to run fsck %s", err)
				}
				if !isClean {
					t.Fatalf("%s should exit 0 on a clean file system %s", fsckCmd, string(logs))
				}
			})

			// And use the same fsck command on a dummy fs to make sure that fails.
			t.Run(fmt.Sprintf("corrupt %s", fsName), func(t *testing.T) {
				if skip {
					t.Skipf("%s not available", fsckCmd)
				}

				logs, isClean, err := Fsck(strings.NewReader(corruptedFs), fsckCmd)
				if err != nil {
					t.Fatalf("failed to run fsck %s", err)
				}
				if isClean {
					t.Fatalf("%s shouldn't exit 0 on a corrupt file system %s", fsckCmd, string(logs))
				}
			})
		})
	}
}
